Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: cleaning up the specs list and swapping it to be a filetree #15483

Closed
wants to merge 1 commit into from

Conversation

JessicaSachs
Copy link
Contributor

This PR rewrites the DOM structure of the SpecList, as well as attempts to decouple the SpecList model from rendering the file tree.

I will need to pair with someone on this PR because it still needs a lot of work to remove technical debt before it is good to merge.

Screen Shot 2021-03-15 at 6 48 32 AM
Screen Shot 2021-03-15 at 7 00 31 AM

  • Closes

User facing changelog

Additional details

How has the user experience changed?

PR Tasks

  • Have tests been added/updated?
  • Has the original issue or this PR been tagged with a release in ZenHub?
  • Has a PR for user-facing changes been opened in cypress-documentation?
  • Have API changes been updated in the type definitions?
  • Have new configuration options been added to the cypress.schema.json?

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Mar 15, 2021

Thanks for taking the time to open a PR!

@cypress
Copy link

cypress bot commented Mar 15, 2021



Test summary

9384 0 119 3Flakiness 1


Run details

Project cypress
Status Passed
Commit f1a4901
Started Mar 15, 2021 11:01 AM
Ended Mar 15, 2021 11:13 AM
Duration 12:22 💡
OS Linux Debian - 10.5
Browser Multiple

View run in Cypress Dashboard ➡️


Flakiness

cypress/integration/retries.ui.spec.js Flakiness
1 runner/cypress retries.ui.spec > opens attempt on each attempt failure for the screenshot, and closes after test passes

This comment has been generated by cypress-bot as a result of this project's GitHub integration settings. You can manage this integration in this project's settings in the Cypress Dashboard

white-space: nowrap;
}

.ul {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curios why classes are named like html tags, looks a little confusing as for me.

@@ -0,0 +1,73 @@
@use '../../index.scss' as *;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@use '../../index.scss' as *;
@import '../../index.scss';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. You want @use. It deduplicates imports and has many other benefits. https://css-tricks.com/introducing-sass-modules/#import-files-with-use

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, how much pain in the ass just to avoid css-in-js :D
I will never understand this probably

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we decide not to use css-in-js?

]

describe('FileExplorer', () => {
it('renders', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any assertions? At least cy.persySnapshot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was using it as a playground, not for specs. It still needs specs... just was trying to get something up for the src to be reviewable.

updateItem(item, idx, { isOpen: !item.isOpen })
}

return (<React.Fragment key={item.shortName}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (<React.Fragment key={item.shortName}>
return (
<React.Fragment key={item.shortName}>

import styles from './FileExplorer.module.scss'
import cs from 'classnames'

const icons: Record<string, any> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const icons: Record<string, any> = {
const icons: Record<string, { icon: typeof reactTs }> = {

Or pass the right type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. There's a lot I don't really understand how to do.

folderClosed: { icon: folderClosed },
}

const getExt = (path: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh that is frontend. Sorry :D

}

return (
<a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use <button for a11y reasons

Copy link
Contributor Author

@JessicaSachs JessicaSachs Mar 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Isn't a more correct because we're doing a hash navigation change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are passing both onClick and href which is really weird. I don't understand this – it looks like you are trying to sync url and state which is a problem all the time. We need to have a single source of trust.

So if you want to use url – you need to subscribe to url changes and use hash without any internal state

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

href is fine as long as onClick is calling preventDefault. This is the "correct" way to build buttons that perform as links, so that you still get standard browser right/middle click behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it is unclear that it is doing prevent default. Maybe not, how would you know from this code? If it is – we need some kind of <Link /> component. But we don't really have any kind of routing, so it looks confusing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

* Split specs into group by their
* first level of folder hierarchy
*
* @param {Array<{name: string}>} specs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks weird, because we have different type in the function declaration

Suggested change
* @param {Array<{name: string}>} specs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this was copy/paste foul.

export type FolderOrFile = File | Folder

export interface File extends FileLike {
type: 'file'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why indention here?


return props.selectedSpecs.includes(item.absolute)
}
}/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}/>
}
/>

@dmtrKovalenko
Copy link
Contributor

Also, it is weird that components like FileTree are inside the design system. Cause it looks like more "end" component, maybe better place will be inside runner-ct?

Copy link
Contributor

@lmiller1990 lmiller1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Some tentative comments, we can pair or I can make a PR to make some improvements. Primary smell is the useState, we should not be maintaining the files list in two places!

isOpen: false,
},
// @ts-ignore
{ name: 'bar/foo.spec.tsx' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need ts-ignore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. It's because I was hacking on this file and hadn't finished the types.

it('renders', () => {
mount(<>
<FileExplorer files={files} />
</>)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need fragment here

return extensionMatches ? extensionMatches[1] : ''
}

export const FileExplorer: React.FC<FileExplorerProps> = (props) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing is quite confusing here, can we make it a bit cleaner


export const FileExplorer: React.FC<FileExplorerProps> = (props) => {
return (<nav className={cs(props.className || '', styles.nav)}><FileTree files={makeFileHierarchy(props.files) || []} isSelected={props.isSelected || (() => {})} onClick={props.onClick || (() => {})}></FileTree></nav>)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need all the || () => {}? Can we just make the props nullable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And even if not, you should use ??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can use ?? with our version of ts? I'm not sure.

Copy link
Contributor

@dmtrKovalenko dmtrKovalenko Mar 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not using ts for compilation it is extremely bad for this (if we are we should stop). Babel will handle this much better. ?? is a stage 3 so preset-env will be enough and I think it will work out of the box

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? is a stage 3 so preset-env will be enough and I think it will work out of the box

https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining if not

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, @dmtrKovalenko, if the TS version we're using doesn't support it, we'll get errors from the LSP


export const FileTree: React.FC<FileTreeProps> = (props) => {
const depth = props.depth || 0
const [files, setFiles] = React.useState(props.files || [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incorrect - now we have two sources of truth. Why do we need two sources of truth? Can't we just use props.files? Design system should not have internal state, ideally everything should be a function of props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I be passing a callback all the way down and making the top-level component mutate files?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it is weird that components like FileTree are inside the design system. Cause it looks like more "end" component, maybe better place will be inside runner-ct?

I think it makes sense because we will use it in runner-ct as well as with the node UI (possibly desktop-gui?). There's also some other benefits.

Technically we should have separate design system and Cypress component library folders (where the only components the design system has are individual atoms), but I don't know that it's worth going to that extreme now.

Yes. You're correct. I'd be happy just to get our design tokens plumbed first and also keep reusable UI patterns separate from domain models.

style?: React.CSSProperties
getHref? (item: FolderOrFile): string
depth: number
onFocus: any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we type these?

@@ -2,6 +2,8 @@ export * from './components/Button'

export * from './components/CypressLogo/CypressLogo'

export * from './components/FileExplorer/FileExplorer'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's idiomatic to add a index.ts that exports everything. export * from './FileExplorer. Then this becomes export * from './components/FileExplorer'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the pattern that @agg23 followed, so I did the same. I don't care one way or another. I've also seen the index.ts pattern and prefer it for convenience, but find it difficult when trying to search for things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care right now. I imagine I will be changing around exports significantly when there starts to be atoms vs final components, so whatever is fine by me.

@JessicaSachs
Copy link
Contributor Author

Also, it is weird that components like FileTree are inside the design system. Cause it looks like more "end" component, maybe better place will be inside runner-ct?

I think it makes sense because we will use it in runner-ct as well as with the node UI (possibly desktop-gui?). There's also some other benefits.

Having it decoupled from the model makes it easier to test and more portable because it's not reliant on the global stylesheets of runner-ct.

Additionally, if we know we'll reuse the pattern, it'll be better to decouple from domain model + global styles early on before runner-ct's style leaks into the component. Once the component's styles are polluted, it's very hard and risky to split them out.

@agg23
Copy link
Contributor

agg23 commented Mar 15, 2021

Also, it is weird that components like FileTree are inside the design system. Cause it looks like more "end" component, maybe better place will be inside runner-ct?

I think it makes sense because we will use it in runner-ct as well as with the node UI (possibly desktop-gui?). There's also some other benefits.

Technically we should have separate design system and Cypress component library folders (where the only components the design system has are individual atoms), but I don't know that it's worth going to that extreme now.

@dmtrKovalenko
Copy link
Contributor

This file explorer should not be a part of any shared library because it is too coupled with a context of runner-ct. Making it decoupled will be a pain I think. So I don't see any problems in putting it right in the runner-ct. Am I missing something?

Comment on lines +71 to +74
onClick={(e) => {
props.onClick(e, props.item)
onClick(e, props.item)
}}
Copy link
Contributor

@dmtrKovalenko dmtrKovalenko Mar 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move it to function for readability

function handleClick(e: React.MouseEvent<HtmlLinkElement>) { 
   props.onClick(e, props.item);
   
   if (props.item.onClick) {
      props.item.onClick(e, props.item)
   }
}

onClick={handleClick}

{ props.item.isSelected }
<li
style={{ ...props.style, ...inlineStyles.li }}
className={styles.li}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
className={styles.li}>
className={styles.li}
>

@JessicaSachs
Copy link
Contributor Author

This file explorer should not be a part of any shared library because it is too coupled with a context of runner-ct. Making it decoupled will be a pain I think. So I don't see any problems in putting it right in the runner-ct. Am I missing something?

I mean... it works and it's easy to test, and will pay us forward if we use it for non-specs (workbench mode? stories? idk). I don't see a reason to revert it.

In general, I don't really see a reason not to make most things decoupled from domain objects... if it's presentational, make it presentational 🤷🏻 Let's DM sometime, I'm curious as to why it's a good idea to couple.

@lmiller1990
Copy link
Contributor

lmiller1990 commented Mar 16, 2021

I am on board with @dmtrKovalenko regarding where this belongs. At least to me, a design system should be more generic. Things like buttons, lists, modals, etc. A spec list doesn't really fit into this in my opinion. What would belong here is a <TreeView>. This Spec List is coupled to specs (spec-specific icons, checking for extensions, etc).

I think the way to make it decoupled would be something like

<TreeView
  leafComponent={Folder}
  nodeComponent={File}
  data={specHierarchy}
/>

(Trivial example). It's similar to using <template> and <slot> in Vue.

Then you could re-use it for various other tree views (perhaps someone else needs a tree view, but does not want to have icons, etc). In Vue this is called a "renderless component". React often refers to this pattern is "render as a function", I think. So what we would need is

  1. Generic <TreeView> component (no render, just the model)
  2. Spec List components. <File>, <Folder> etc. This is where things like icons and extension logic might live.
  3. Compose them together to create a <SpecList>.

Anyway, I think we can discuss this further, for now I will push some simple changes to get this over the line and fix the tests.

Regarding CSS modules vs CSS-in-JS, I don't have much experience around either, it's super annoying how you cannot see the class names in the DOM since CSS modules make them into a hash. I wonder if there is some flag to show something a bit more readable in the DOM.

@JessicaSachs
Copy link
Contributor Author

Closing in favor of #15513

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants